package org.activequant.tradesystem.ib;

import java.util.concurrent.atomic.AtomicBoolean;

import org.activequant.broker.IOrderTracker;
import org.activequant.core.domainmodel.account.Order;
import org.activequant.core.domainmodel.events.OrderAcceptEvent;
import org.activequant.core.domainmodel.events.OrderCancelEvent;
import org.activequant.core.domainmodel.events.OrderCommissionEvent;
import org.activequant.core.domainmodel.events.OrderCompletionEvent;
import org.activequant.core.domainmodel.events.OrderEvent;
import org.activequant.core.domainmodel.events.OrderExecutionEvent;
import org.activequant.core.domainmodel.events.OrderSubmitEvent;
import org.activequant.core.domainmodel.events.OrderUpdateEvent;
import org.activequant.core.types.OrderSide;
import org.activequant.core.types.OrderType;
import org.activequant.core.types.TimeStamp;
import org.activequant.util.messaging.ISimpleMessagingService;
import org.activequant.util.pattern.events.IEventListener;
import org.apache.log4j.Logger;
import org.jfree.chart.annotations.XYPointerAnnotation;
import org.jfree.ui.TextAnchor;

import com.ib.algokit.AnalyzerPluginInterface;
import com.ib.algokit.InstrumentAnalyzer;
import com.ib.algokit.TraderProxy;
import com.ib.algokit.marketscan.MarketScanChangeListenerHandlingStrategy;

/**
 * Closes a position, and if executed, lets the instrumentAnalyzer know not to listen to any more real time data for this instrument.
 * Optionally (if configured) may communicate
 * the entry/exit info to the messenger service.<p>
 * BracketPositionService implements IPositionService.<br/>
 * Holds the following associated variables:
 * <ul>
 * <li>broker(IBroker)</li>
 * <li>trailingDistance(double)</li>
 * <li>msg(ISimpleMessagingService)</li>
 * </ul>
 *
 *  @author dmisev
 */
public class ClosePositionService implements IPositionService {
	private final Logger log = Logger.getLogger(getClass());
	/**
	 * private final TraderProxy ema;
	 */
	private final TraderProxy ema;	
	/**
	 * private final InstrumentAnalyzer instrumentAnalyzer;
	 */
	private final InstrumentAnalyzer instrumentAnalyzer;
	/**
	 * private double trailingDistance;
	 */
	private double trailingDistance;
	/**
	 * private ISimpleMessagingService msg;
	 */
	private ISimpleMessagingService msg;
	/**
	 * constructs a ClosePositionService(implements IPositionService) using the given instrumentAnalyzer(InstrumentAnalyzer) to set its associated one and ema(TraderProxy)
	 * @param instrumentAnalyzer
	 */
	public ClosePositionService(InstrumentAnalyzer instrumentAnalyzer) {
		this.ema = instrumentAnalyzer.getEMA();
		this.instrumentAnalyzer=instrumentAnalyzer;
	}
	/**
	 * Sets the associated trailingDistance(double) with the given val(double)
	 * @param val
	 */
	public void setTrailingDistance(double val) {
		trailingDistance = val;
	}
	/**
	 * Sets the associated msg(ISimpleMessagingService) with the given val(ISimpleMessagingService)
	 * @param val
	 */
	public void setMessengerService(ISimpleMessagingService val) {
		msg = val;
	}
	/**
	 * If the given side(OrderSide) enum is BUY, returns an OrderSide.SELL enum.<br/>
	 * Otherwise, if the given side(OrderSide) enum is SELL, returns an OrderSide.BUY enum.<br/>
	 * Doesn't support any other cases
	 * @param side
	 * @return
	 */
	private static OrderSide reverseSide(OrderSide side) {
		switch(side) {
		case BUY: return OrderSide.SELL;
		case SELL: return OrderSide.BUY;
		default: throw new IllegalArgumentException("unsupported order side: " + side);
		}
	}
	/**
	 * private final static double EPSILON = 1e-6;
	 */
	private final static double EPSILON = 1e-6;
	/**
	 * Returns whether the absolute difference between the given d1(double) and d2(double) is less than the given epsilon(double)<br/>
	 * <code>return Math.abs(d1 - d2) < epsilon;</code>
	 * @param d1
	 * @param d2
	 * @param epsilon
	 * @return
	 */
	private static boolean doubleEquals(double d1, double d2, double epsilon) {
		return Math.abs(d1 - d2) < epsilon;
	}
	/**
	 * A private nested class. Tracker implements IPositionTracker.<br/>
	 * Holds the following associated variables:
	 * <ul>
	 * <li>originalOrder(Order)</li>
	 * <li>quantity(double)</li>
	 * <li>averagePrice(double)</li>
	 * <li>isClosed(AtomicBoolean)</li>
	 * <li>closeOrderTracker(IOrderTracker)</li>
	 * <li>closeOrderListener(IEventListener&lt;OrderEvent&gt;)</li>
	 * </ul>
	 * @author Dimitar
	 *
	 */
	private class Tracker implements IPositionTracker {
		/**
		 * private final Order originalOrder;
		 */
		private final Order originalOrder;
		
		/**
		 * private final AtomicBoolean isClosed = new AtomicBoolean();
		 */
		private final AtomicBoolean isClosed = new AtomicBoolean();
		/**
		 * private final IOrderTracker closeOrderTracker;
		 */
		private final IOrderTracker closeOrderTracker;
		
		private TimeStamp mainOrderAcceptedTimeStamp=null;
		/**
		 * private TimeStamp mainOrderLastExecutionTimeStamp=null;
		 */
		private TimeStamp mainOrderLastExecutionTimeStamp=null;
		/**
		 * private TimeStamp mainOrderCompletedAndCommissionsReceivedTimeStamp=null;
		 */
		private TimeStamp mainOrderCompletedAndCommissionsReceivedTimeStamp=null;
		/**
		 * private boolean plotMainOrderAcceptEvents=true;
		 */
		private boolean plotMainOrderAcceptEvents=true;
		/**
		 * private boolean plotMainOrderExecutionEvents=true;
		 */
		private boolean plotMainOrderExecutionEvents=true;
		/**
		 * private boolean plotMainOrderCommissionEvents=false;
		 */
		private boolean plotMainOrderCommissionEvents=false;
		/**
		 * private boolean plotMainOrderCancelEvents=true;
		 */
		private boolean plotMainOrderCancelEvents=true;
		/**
		 * private double mainOrderFilledQuantity;<br/>
		 * actual quantity of opened position (as per market order execution)
		 */
		private double mainOrderFilledQuantity;
		/**
		 * private double mainOrderAveragePrice=0;<br/>
		 * actual price of opened position (without the commission)
		 */
		private double mainOrderAveragePrice=0;
		/**
		 * private double mainOrderCommission=0;<br/>
		 * This is the money I spent (so far) on opening this position in commissions, gets updated every time an execution happens for the main order
		 */
		private double mainOrderCommission=0;
		/**
		 * private final IEventListener&lt;OrderEvent&gt; mainOrderListener = new IEventListener&lt;OrderEvent&gt;() {...}<br/>
		 * A listener whose job is to react on OrderCompletionEvents only. Once it receives a completion(OrderCompletionEvent),
		 * it sets the associated quantity(double) and price(double) with the totalQuantity(double) and averagePrice(double) of that completion(OrderCompletionEvent)<br/>
		 * Then it tries to open a TRAILING_STOP Order to lock in any profits:<br/>
		 * -If no position has been opened already, returns without doing anything else.<br/>
		 * -Otherwise, if we already asked to close the position (and it exists) it will create/submit a MARKET Order to get rid of the position.<br/>
		 * -Otherwise, everything is good and it creates.submits a TRAILING_STOP with trailingDistance=${distance} to lock in the profits if price starts dropping
		 */
		private final IEventListener<OrderEvent> closeOrderListener = new IEventListener<OrderEvent>() {
						
			public void eventFired(OrderEvent event) {
				//printOrder("Main Order event", event);
				if (event instanceof OrderSubmitEvent) {
//					OrderSubmitEvent submit = (OrderSubmitEvent) event;
					// nothing to do
				}else if (event instanceof OrderAcceptEvent) {
					OrderAcceptEvent accepted = (OrderAcceptEvent) event;
					// here the main order has been accepted
					mainOrderAcceptedTimeStamp=accepted.getEventTimeStamp();
					if(plotMainOrderAcceptEvents){
						Order order=closeOrderTracker.getOrder();
						InstrumentAnalyzer instrumentAnalyzer=InstrumentAnalyzer.getInstrumentAnalyzers().get(order.getInstrumentSpecification());
						if(instrumentAnalyzer!=null&&instrumentAnalyzer.getAnalyzerPlugin()!=null&&instrumentAnalyzer.getAnalyzerPlugin().getTimeseriesCombinedChart()!=null){
							double price=order.getLimitPrice();
							AnalyzerPluginInterface analyzerUtils=instrumentAnalyzer.getAnalyzerPlugin();
							XYPointerAnnotation annotation = new XYPointerAnnotation("A", mainOrderAcceptedTimeStamp.getDate().getTime(), price, 1.570796326794897D);
							annotation.setTextAnchor(TextAnchor.TOP_CENTER);
							String tooltip=getOrderEventToolTip(order, accepted, "Closing Order ", true);
							annotation.setToolTipText(tooltip);
							analyzerUtils.getTimeseriesCombinedChart().addAnnotation(annotation);
						}
					}
				}else if (event instanceof OrderCompletionEvent) {
					// here the position has been opened
					OrderCompletionEvent completion = (OrderCompletionEvent) event;
					mainOrderFilledQuantity = completion.getTotalQuantity();
					mainOrderAveragePrice = completion.getAveragePrice();
					//positionCost=quantity*averagePriceToOpenPosition+totalCommissionToOpenPosition;
				}else if (event instanceof OrderExecutionEvent) {
					// here the position has been opened
					OrderExecutionEvent execution = (OrderExecutionEvent) event;
					mainOrderFilledQuantity = execution.getCumulatedQuantity();
					mainOrderAveragePrice = execution.getAveragePrice();
					mainOrderLastExecutionTimeStamp=execution.getEventTimeStamp();
					if(plotMainOrderExecutionEvents){
						Order order=closeOrderTracker.getOrder();
						InstrumentAnalyzer instrumentAnalyzer=InstrumentAnalyzer.getInstrumentAnalyzers().get(order.getInstrumentSpecification());
						if(instrumentAnalyzer!=null&&instrumentAnalyzer.getAnalyzerPlugin()!=null&&instrumentAnalyzer.getAnalyzerPlugin().getTimeseriesCombinedChart()!=null){
							AnalyzerPluginInterface analyzerUtils=instrumentAnalyzer.getAnalyzerPlugin();
							XYPointerAnnotation annotation = new XYPointerAnnotation("E", mainOrderLastExecutionTimeStamp.getDate().getTime(), mainOrderAveragePrice, -Math.PI/2);
							String tooltip=getOrderEventToolTip(order, execution, "Closing Order ", true);
							annotation.setToolTipText(tooltip);
							annotation.setTextAnchor(TextAnchor.BOTTOM_CENTER);
							analyzerUtils.getTimeseriesCombinedChart().addAnnotation(annotation);
						}
					}
					//positionCost=quantity*averagePriceToOpenPosition+totalCommissionToOpenPosition;
				}else if (event instanceof OrderCommissionEvent) {
					// here the position has been opened
					OrderCommissionEvent commission = (OrderCommissionEvent) event;
					Order order=closeOrderTracker.getOrder();
					if(plotMainOrderCommissionEvents){
						
						InstrumentAnalyzer instrumentAnalyzer=InstrumentAnalyzer.getInstrumentAnalyzers().get(order.getInstrumentSpecification());
						if(instrumentAnalyzer!=null&&instrumentAnalyzer.getAnalyzerPlugin()!=null&&instrumentAnalyzer.getAnalyzerPlugin().getTimeseriesCombinedChart()!=null){
							AnalyzerPluginInterface analyzerUtils=instrumentAnalyzer.getAnalyzerPlugin();
							XYPointerAnnotation annotation = new XYPointerAnnotation("C", commission.getEventTimeStamp().getDate().getTime(), mainOrderAveragePrice, 1.570796326794897D);
							annotation.setTextAnchor(TextAnchor.TOP_CENTER);
							String tooltip=getOrderEventToolTip(order, commission, "Closing Order ", true);
							annotation.setToolTipText(tooltip);
							analyzerUtils.getTimeseriesCombinedChart().addAnnotation(annotation);
						}
					}
					if(commission.isFinalCommissionEvent()){
						mainOrderCommission=commission.getCommission();
//						mainOrderCompletedAndCommissionsReceivedTimeStamp=ema.getTimeStampService().currentTimeStamp();
						//positionCost=quantity*averagePriceToOpenPosition+totalCommissionToOpenPosition;
						mainOrderCompletedAndCommissionsReceivedTimeStamp=commission.getEventTimeStamp();
						if(originalOrder.getOrderSide()==OrderSide.BUY){
							log.info("bought "+mainOrderFilledQuantity+""+originalOrder.getInstrumentSpecification().getSymbol().toString()+" for a "+((mainOrderAveragePrice*mainOrderFilledQuantity)+mainOrderCommission));
						}else if(originalOrder.getOrderSide()==OrderSide.SELL){
							log.info("sold "+mainOrderFilledQuantity+""+originalOrder.getInstrumentSpecification().getSymbol().toString()+" and got "+((mainOrderAveragePrice*mainOrderFilledQuantity)-mainOrderCommission));
						}
//						InstrumentAnalyzer instrumentAnalyzer=InstrumentAnalyzer.getInstrumentAnalyzers().get(order.getInstrumentSpecification());
						MarketScanChangeListenerHandlingStrategy threadPerInstrumentEventHandlingStrategy=(MarketScanChangeListenerHandlingStrategy)ema.getMarketScanChangeListener();
						threadPerInstrumentEventHandlingStrategy.removeInstrumentAnalyzer(order.getInstrumentSpecification(),true, true);
						
					}else{
						mainOrderCommission=commission.getCommission();
					}
				}else if (event instanceof OrderCancelEvent) {
					// here the position has been opened
					OrderCancelEvent cancel = (OrderCancelEvent) event;
					if(plotMainOrderCancelEvents){
						Order order=closeOrderTracker.getOrder();
						InstrumentAnalyzer instrumentAnalyzer=InstrumentAnalyzer.getInstrumentAnalyzers().get(order.getInstrumentSpecification());
						if(instrumentAnalyzer!=null&&instrumentAnalyzer.getAnalyzerPlugin()!=null&&instrumentAnalyzer.getAnalyzerPlugin().getTimeseriesCombinedChart()!=null){
							AnalyzerPluginInterface analyzerUtils=instrumentAnalyzer.getAnalyzerPlugin();
							double price=order.getLimitPrice();
							XYPointerAnnotation annotation = new XYPointerAnnotation("Cl", cancel.getEventTimeStamp().getDate().getTime(), price, 1.570796326794897D);
							annotation.setTextAnchor(TextAnchor.TOP_CENTER);
							String tooltip=getOrderEventToolTip(order, cancel, "Closing Order ", true);
							annotation.setToolTipText(tooltip);
							analyzerUtils.getTimeseriesCombinedChart().addAnnotation(annotation);
						}
					}
				}				
			}
		};
		/**
		 * private IOrderTracker stopOrderTracker;
		 */
		private IOrderTracker stopOrderTracker;
		/**
		 * Constructs a Tracker(implements IPositionTracker) using the given order(Order) to set its associated originalOrder(Order) (and submits the given order(Order)):<br/>
		 * - validates and prepares that order(Order) for submission by invoking <code>broker.prepareOrder(order)</code> on the broker(IBroker) and sets the associated mainOrderTracker(IOrderTracker) with the resulting IOrderTracker.<br/>
		 * - registers the associated mainOrderListener(IEventListener&lt;OrderEvent&gt;) to listen for OrderEvents from the associated mainOrderTracker(IOrderTracker) <br/>
		 * - submits the order by invoking <code>mainOrderTracker.submit()</code> on the associated mainOrderTracker(IOrderTracker).
		 * @param order
		 */
		public Tracker(Order order) {
			originalOrder = order;
			closeOrderTracker = ema.getBroker().prepareOrder(order);
			closeOrderTracker.getOrderEventSource().addEventListener(closeOrderListener);
			closeOrderTracker.submit();
		}
		/**
		 * <strong>1.</strong> Gets the previous value(boolean) of the associated isClosed(AtomicBoolean) and sets its new value to true.<br/>
		 * If that previous value(boolean) is true, it returns without doing anything else.<br/>
		 * <strong>2.</strong> Tries to cancel (if not filled yet) the associated originalOrder(Order) by invoking <code>mainOrderTracker.cancel()</code> on the associated mainOrderTracker(IOrderTracker).<br/>
		 * <strong>3.</strong> If the associated quantity(double) is greater than 0, it gets the TRAILING_STOP Order from the associated stopOrderTracker(IOrderTracker), sets its
		 * trailingDistance(double) to 0, and updates the pending order by invoking <code>stopOrderTracker.update(order)</code> on the associated stopOrderTracker(IOrderTracker)
		 */
		public void close() {
			if(isClosed.getAndSet(true)) {
				return; // already closed
			}
			// if main order not yet filled, cancel it...
			closeOrderTracker.cancel();
			
			//limitOrderTracker.cancel();
			//stopOrderTracker.cancel();
			
			if(mainOrderFilledQuantity > 0.0) {
				// have to close the position. Lets do it by moving
				// trailing distance to zero
				Order order = new Order(stopOrderTracker.getOrder());
				order.setOrderType(OrderType.MARKET);
				//order.setTrailingDistance(0.0);
				stopOrderTracker.update(order);
			}
		}
		/**
		 * returns the current value of the associated isClosed(AtomicBoolean)
		 */
		public boolean isClosed() {
			return isClosed.get();
		}
		/**
		 * {@inheritDoc}
		 */
		public double getQuantity() {
			throw new UnsupportedOperationException();
		}
	}
	/**
	 * Creates and returns a Tracker(implements IPositionTracker) using the given order(Order) to set its associated originalOrder(Order) (and submits the given order(Order) asynchronously):<br/>
	 * - validates and prepares that order(Order) for submission by invoking <code>broker.prepareOrder(order)</code> on the broker(IBroker) and sets the associated mainOrderTracker(IOrderTracker) with the resulting IOrderTracker.<br/>
	 * - registers the associated mainOrderListener(IEventListener&lt;OrderEvent&gt;) to listen for OrderEvents from the associated mainOrderTracker(IOrderTracker) <br/>
	 * - submits the order by invoking <code>mainOrderTracker.submit()</code> on the associated mainOrderTracker(IOrderTracker).
	 */
	public IPositionTracker openPosition(Order order) {
		return new Tracker(new Order(order));
	}
	
	public String getOrderEventToolTip(Order order, OrderEvent event, String prefix, boolean includeTime){
		StringBuilder tooltip=prefix!=null?new StringBuilder(prefix):new StringBuilder();
		if (event instanceof OrderAcceptEvent){
			tooltip.append("Accepted:");
			tooltip.append(" "+order.getOrderSide());
			tooltip.append(" "+order.getOrderType());
			if(order.getOrderType()==OrderType.LIMIT){
				tooltip.append(" "+order.getLimitPrice());
			}
			tooltip.append(" Qty:"+order.getQuantity());
		}else if (event instanceof OrderExecutionEvent) {
			OrderExecutionEvent execution = (OrderExecutionEvent) event;
			tooltip.append("Executed: "+execution.getCumulatedQuantity()+" cum.qty. "+execution.getAveragePrice()+" avg. price");
		}else if (event instanceof OrderCommissionEvent) {
			OrderCommissionEvent commission = (OrderCommissionEvent) event;
			if(commission.isFinalCommissionEvent()){
				tooltip.append("Final ");
			}
			tooltip.append("Commission: "+commission.getCommission());
		}else if (event instanceof OrderCancelEvent) {
			OrderCancelEvent cancel = (OrderCancelEvent) event;
			tooltip.append("Cancelled: "+cancel.getMessage());
		}else if (event instanceof OrderUpdateEvent){
			tooltip.append("Updated:");
			tooltip.append(" "+order.getOrderSide());
			tooltip.append(" "+order.getOrderType());
			if(order.getOrderType()==OrderType.LIMIT){
				tooltip.append(" "+order.getLimitPrice());
			}
			tooltip.append(" Qty:"+order.getQuantity());
		}
		if(includeTime){
			tooltip.append(" Time:"+event.getEventTimeStamp().getDate());
		}
		return tooltip.toString();
	}
	
}
